<?php
namespace app\components\pay;

use Yii;
use yii\base\Exception;

use app\components\Func;
use app\components\Mcrypt;

use app\models\Order;
use app\models\OrderPay;

/**
 * 普通商户微信支付(3.3)
 */
class WxpayMch extends Pay {

    protected $notify_url = '/q/success-mch';
    protected $url_query = 'https://api.mch.weixin.qq.com/pay/orderquery';//查询
    protected $url_refund =  'https://api.mch.weixin.qq.com/secapi/pay/refund';//退款申请接口
    protected $url_refund_query =  'https://api.mch.weixin.qq.com/pay/refundquery';//退款查询接口
    protected $url_prepay = "https://api.mch.weixin.qq.com/pay/unifiedorder";//接口链接
    protected $url_micropay = "https://api.mch.weixin.qq.com/pay/micropay";//被扫支付
    protected $url_cancel = "https://api.mch.weixin.qq.com/secapi/pay/reverse";//被扫定单撤销
    protected $url_authcodetoopenid = "https://api.mch.weixin.qq.com/tools/authcodetoopenid";//根据支付码获取openid

    //预支付
    public function prepay(array $paras, $nums = 0) {
        if($nums > 3) throw new \Exception('微信预支付接口调用次数过多,请稍后',2022030);
        unset($paras['sign']);
        if(!isset($paras['body'])) throw new \Exception('body为空',2022017);
        if(!isset($paras['out_trade_no'])) throw new \Exception('out_trade_no为空',2022015);
        if(!isset($paras['total_fee'])) throw new \Exception('total_fee为空',2022016);
        if(!isset($paras['trade_type'])) throw new \Exception('trade_type为空');
        if(!isset($paras['notify_url'])) $paras['notify_url'] = $this->notifyUrl;
        if(!isset($paras['appid'])) $paras['appid'] = $this->appid;
        if(!isset($paras['mch_id'])) $paras['mch_id'] = $this->paymentObj->partner_id;
        //JSAPI必须传openid
        if($paras['trade_type'] == 'JSAPI'){
            if(isset($paras['sub_appid']) && !isset($paras['sub_openid'])) throw new \Exception('sub_openid为空');
        }
        $paras['nonce_str'] = Func::getcode(32);
        $paras['spbill_create_ip'] = Yii::$app->request->userIP;
        $paras['sign'] = $this->getSign($paras);
        //Yii::error(json_encode($paras));
        $result = Func::postData($this->url_prepay,Func::arrayToXml($paras));
        if(empty($result)) throw new \Exception('微信接口提示:可能是超时', 2022030);
        $arr = Func::xmlToArray($result);
        if($arr['return_code'] != 'SUCCESS') throw new \Exception('微信接口提示:'.$result,2022030);
        if($arr['result_code'] != 'SUCCESS'){
            if($arr['err_code'] == 'SYSTEMERROR') return call_user_func_array([$this,__METHOD__], [$paras, ++$nums]);
            throw new \Exception('微信接口提示:'.$result,2022030);
        }
        return $arr;
    }
    //根据支付码获取openid
    public function authcodeToOpenid(array $paras) {
        if(!isset($paras['auth_code'])) throw new \Exception('auth_code为空',2022020);
        if(!isset($paras['appid'])) $paras['appid'] = $this->appid;
        if(!isset($paras['mch_id'])) $paras['mch_id'] = $this->paymentObj->partner_id;
        $paras['nonce_str'] = Func::getcode(32);
        $paras['spbill_create_ip'] = Yii::$app->request->userIP;
        $paras['sign'] = $this->getSign($paras);
        //写入java库
        $result = Func::postData($this->url_authcodetoopenid,Func::arrayToXml($paras));
        if(empty($result)) throw new \Exception('微信接口返回空',2022030);

        $arr = Func::xmlToArray($result);
        if($arr['return_code'] != 'SUCCESS')  throw new \Exception('微信接口提示:'.$arr['return_msg'],2022030);
        if($arr['result_code'] != 'SUCCESS')  throw new \Exception('微信接口提示:'.$arr['err_code'].$result,2022030);
        return $arr;
    }
    //刷卡支付
    public function micropay(array $paras) {
        if(!isset($paras['body'])) throw new \Exception('body为空',2022017);
        if(!isset($paras['out_trade_no'])) throw new \Exception('out_trade_no为空',2022015);
        if(!isset($paras['total_fee'])) throw new \Exception('total_fee为空',2022016);
        if(!isset($paras['auth_code'])) throw new \Exception('auth_code为空',2022020);
        if(!isset($paras['appid'])) $paras['appid'] = $this->appid;
        if(!isset($paras['mch_id'])) $paras['mch_id'] = $this->paymentObj->partner_id;
        $paras['nonce_str'] = Func::getcode(32);
        $paras['spbill_create_ip'] = Yii::$app->request->userIP;
        $paras['sign'] = $this->getSign($paras);
        //写入java库
        $result = Func::postData($this->url_micropay,Func::arrayToXml($paras));
        if(empty($result)) return $this->micropayquery(['out_trade_no'=> $paras['out_trade_no']]);
        $arr = Func::xmlToArray($result);
        if($arr['return_code'] != 'SUCCESS') return $this->micropayquery(['out_trade_no'=> $paras['out_trade_no']]);
        if($arr['result_code'] == 'SUCCESS') return $arr;
        switch($arr['err_code']){
            case 'PARAM_ERROR':
            case 'ORDERPAID':
            case 'NOAUTH':
            case 'AUTHCODEEXPIRE':
            case 'NOTENOUGH':
            case 'NOTSUPORTCARD':
            case 'ORDERCLOSED':
            case 'ORDERREVERSED':
            case 'AUTH_CODE_ERROR':
            case 'AUTH_CODE_INVALID':
            case 'XML_FORMAT_ERROR':
            case 'SIGNERROR':
            case 'BUYER_MISMATCH':
            case 'APPID_NOT_EXIST':
            case 'MCHID_NOT_EXIST':
            case 'OUT_TRADE_NO_USED':
            case 'APPID_MCHID_NOT_MATCH':
                throw new \Exception('微信接口提示:'.$arr['err_code_des'].$result,2022030);
                break;
            default:
                return $this->micropayquery(['out_trade_no'=> $paras['out_trade_no']]);
                break;
        }
    }
    //被扫刷卡支付重复查询或执行撤销
    protected function micropayquery(array $paras, $nums = 0){
        if($nums > 12){
            $this->cancel($paras);
            throw new \Exception('输入刷卡密码超时,定单已撤销,请重新生成定单并支付', 10001);
        }
        sleep(5);
        $arr = $this->orderquery($paras);
        $trade_state = isset($arr['trade_state']) ? $arr['trade_state'] : 'ORDERNOTEXIST';
        switch($trade_state){
            case 'SUCCESS':
                //如果网络不好,jsapi支付成功被扫也报错
                if($arr['trade_type'] == 'JSAPI'){
                    throw new \Exception('微信JSAPI支付成功,不能重复支付');
                }
                return $arr;
                break;
            case 'REFUND':
            case 'CLOSED':
            case 'ORDERNOTEXIST':
            case 'REVOKED':
                $desc = isset($arr['trade_state_desc']) ? $arr['trade_state_desc'] : json_encode($arr, JSON_UNESCAPED_UNICODE);
                throw new \Exception('微信接口提示:'.$desc,2022030);
                break;
            default:
                return call_user_func_array([$this,__METHOD__], [$paras, ++$nums]);
                break;
        }
    }
    //定单查询,保证返回查询结果(成功/失败),如果超时或接口返回为空则重复查询
    public function orderquery(array $paras, $nums = 0) {
        if($nums > 3) throw new \Exception('微信定单查询次数过多', 2022030);
        if($nums > 0)sleep(1);//如果多次查询,每次延时
        unset($paras['sign']);
        if(!isset($paras['out_trade_no']))throw new \Exception('out_trade_no为空',2022015);
        if(!isset($paras['appid'])) $paras['appid'] = $this->appid;
        if(!isset($paras['mch_id'])) $paras['mch_id'] = $this->paymentObj->partner_id;
        $paras['nonce_str'] = Func::getcode(32);
        $paras['sign'] = $this->getSign($paras);
        $result = Func::postData($this->url_query, Func::arrayToXml($paras));
        if(empty($result)) return call_user_func_array([$this,__METHOD__], [$paras, ++$nums]);
        $arr = Func::xmlToArray($result);
        if($arr['return_code'] != 'SUCCESS'){
            return call_user_func_array([$this,__METHOD__], [$paras, ++$nums]);
        }
        if($arr['result_code'] != 'SUCCESS' && $arr['err_code'] == 'SYSTEMERROR') {
            return call_user_func_array([$this,__METHOD__], [$paras, ++$nums]);
        }
        return $arr;
    }
    
    /**
     * 微信 退款
     * @param object $orderPay  订单支付数据
     * @param int $refund_fee   退款金额
     * @auth qiaohongbo@kelaile.cn
     */
    public function refund(array $paras) {
        if(!isset($paras['transaction_id'])) throw new \Exception('transaction_id为空');
        if(!isset($paras['total_fee'])) throw new \Exception('total_fee为空',2022016);
        if(!isset($paras['refund_fee'])) throw new \Exception('refund_fee为空');
        if(!isset($paras['device_info'])) throw new \Exception('device_info为空');
        if(!isset($paras['op_user_id'])) throw new \Exception('op_user_id为空');
        if(!isset($paras['appid'])) $paras['appid'] = $this->appid;
        if(!isset($paras['mch_id'])) $paras['mch_id'] = $this->paymentObj->partner_id;
        $paras['nonce_str'] = Func::getcode(32);
        $paras['out_refund_no'] = Mcrypt::getcode(10);
        $paras['sign'] = $this->getSign($paras);
        //$result = Func::postData($this->url_refund,Func::arrayToXml($paras));
        $pemCert = $this->getPemCert();
        $pemKey = $this->getPemKey();
        $result = Func::postData($this->url_refund, array(
            CURLOPT_POSTFIELDS => Func::arrayToXml($paras),
            CURLOPT_SSLCERT => $pemCert,
            CURLOPT_SSLKEY => $pemKey,
        ));
        //删除临时证书文件
        if(file_exists($pemCert))unlink($pemCert);
        if(file_exists($pemKey))unlink($pemKey);
        //if(empty($result)) return $this->refundquery(['out_trade_no'=> $paras['out_trade_no']]);
        if(empty($result)) throw new \Exception('微信接口返回为空',2022034);
        $arr = Func::xmlToArray($result);
        if(empty($arr)) throw new \Exception('微信接口返回格式错误'.$result,2022030);

        if($arr['return_code'] != 'SUCCESS') throw new \Exception($arr['return_msg'],2022045);
        if($arr['result_code'] != 'SUCCESS') throw new \Exception($arr['err_code'].'-'.$arr['err_code_des'],2022045);
        return [
            'refund_trade_no' => $paras['out_refund_no'],
            'refund_trade_id' => $arr['refund_id'],
        ];
    }
    
    //退款查询
    public function refundquery(array $paras, $nums = 0){
        if($nums > 6) throw new \Exception('退款查询次数过多,请稍后');
        unset($paras['sign']);
        if(!isset($paras['out_trade_no']))throw new \Exception('out_trade_no为空',2022015);
        if(!isset($paras['appid'])) $paras['appid'] = $this->appid;
        if(!isset($paras['mch_id'])) $paras['mch_id'] = $this->paymentObj->partner_id;
        $paras['nonce_str'] = Func::getcode(32);
        $paras['op_user_id'] = $this->paymentObj->partner_id;
        $paras['sign'] = $this->getSign($paras);
        $result = Func::postData($this->url_refund_query,Func::arrayToXml($paras));
        if(empty($result)) return call_user_func_array([$this,__METHOD__], [$paras, ++$nums]);
        $arr = Func::xmlToArray($result);
        if($arr['result_code'] != 'SUCCESS') return $this->refundquery(['out_trade_no'=> $paras['out_trade_no']]);
        if($arr['return_code'] != 'SUCCESS') throw new \Exception('微信接口提示:'.$result,2022030);
        if($arr['refund_status_0'] != 'SUCCESS')throw new \Exception('微信接口提示:'.$result,2022030);
        return $arr;

    }
    //撤销
    public function cancel(array $paras, $nums = 0){
        if($nums > 3) throw new \Exception('微信撤销定单调用次数过多,请稍后');
        unset($paras['sign']);
        if(!isset($paras['out_trade_no']))throw new \Exception('out_trade_no为空',2022015);
        if(!isset($paras['appid'])) $paras['appid'] = $this->appid;
        if(!isset($paras['mch_id'])) $paras['mch_id'] = $this->paymentObj->partner_id;
        $paras['nonce_str'] = Func::getcode(32);
        $paras['sign'] = $this->getSign($paras);
        //$result = Func::postData($this->url_refund,Func::arrayToXml($paras));
        $pemCert = $this->getPemCert();
        $pemKey = $this->getPemKey();
        $result = Func::postData($this->url_cancel, array(
            CURLOPT_POSTFIELDS => Func::arrayToXml($paras),
            CURLOPT_SSLCERT => $pemCert,
            CURLOPT_SSLKEY => $pemKey,
        ));
        //删除临时证书文件
        if(file_exists($pemCert))unlink($pemCert);
        if(file_exists($pemKey))unlink($pemKey);
        if(empty($result)) return $this->orderquery(['out_trade_no'=> $paras['out_trade_no']]);
        $arr = Func::xmlToArray($result);
        if($arr['return_code'] != 'SUCCESS') throw new \Exception('微信接口提示:'.$result,2022030);
        if($arr['recall'] == 'Y'){
            sleep(10);
            return call_user_func_array([$this,__METHOD__], [$paras, ++$nums]);
        }
        if($arr['result_code'] != 'SUCCESS'){
            if($arr['err_code'] != 'SYSTEMERROR')throw new \Exception('微信接口提示:'.$result,2022030);
            $query_arr = $this->orderquery($paras);
            if($query_arr['trade_state'] != 'REVOKED')throw new \Exception('微信撤销定单失败:' . Func::arrayToXml($query_arr));
            return $query_arr;
        }
        return $arr;
    }
    //证书
    public function getPemCert(){
        return $this->paymentObj->getPemCert(isset($this->paymentObj->mchid) ? $this->paymentObj->mchid : '');
    }
    public function getPemKey(){
        return $this->paymentObj->getPemKey(isset($this->paymentObj->mchid) ? $this->paymentObj->mchid : '');
    }
    /**
     * 	作用：生成签名
     */
    public function getSign($Obj)
    {
        foreach ($Obj as $k => $v)
        {
            $Parameters[$k] = (string)$v;
        }
        //签名步骤一：按字典序排序参数
        ksort($Parameters, SORT_STRING);

        //$String = $this->formatBizQueryParaMap($Parameters, false);
        $String = Func::http_build_str($Parameters);
        //echo '【string1】'.$String.'</br>';
        //签名步骤二：在string后加入KEY
        $String = $String."&key=".$this->partner_key;
        //echo "【string2】".$String."</br>";
        //签名步骤三：MD5加密
        $String = md5($String);
        //echo "【string3】 ".$String."</br>";
        //签名步骤四：所有字符转为大写
        $result_ = strtoupper($String);
        //echo "【result】 ".$result_."</br>";
        return $result_;
    }
    //验证支付成功通知sign
    public function checkNotifySign($arr)
    {
        $sign = $arr['sign'];
        unset($arr['sign']);
        return $sign == $this->getSign($arr);
    }
    
    public function getJsapi(array $paras){
        if(!isset($paras['openid'])) throw new \Exception('openid为空');
        if(!isset($paras['body'])) throw new \Exception('body为空',2022017);
        if(!isset($paras['out_trade_no'])) throw new \Exception('out_trade_no为空',2022015);
        if(!isset($paras['total_fee'])) throw new \Exception('total_fee为空',2022016);
        if(!isset($paras['notify_url'])) throw new \Exception('notify_url为空',2022024);
        $paras['trade_type']='JSAPI';
        $arr = $this->prepay($paras);

        $jsapi_arr = [];
        $jsapi_arr["appId"] = $this->appid;
        $jsapi_arr["timeStamp"] = (string)time();
        $jsapi_arr["nonceStr"] = Mcrypt::getcode(32);
        $jsapi_arr["package"] = "prepay_id=".$arr['prepay_id'];
        $jsapi_arr["signType"] = "MD5";
        $jsapi_arr["paySign"] = $this->getSign($jsapi_arr);
        return json_encode($jsapi_arr);
    }
    //定单查询并并重置支付成功结果，忽略非支付成功
    /*public function queryOrderAndRsetPaySuccess($order)
    {
        if(empty($order)) throw new \Exception('订单不存在',2021024);

        $result_arr = $this->orderquery(['out_trade_no' => $order->our_trade_no]);
        switch($result_arr['trade_state'])
        {
            case "SUCCESS":
            {
                $pay_type = $result_arr['trade_type'] == 'MICROPAY' ? OrderPay::PAY_TYPE_WX_MICROPOS : OrderPay::PAY_TYPE_WX;
                $order->status = Order::STATUS_PAY_END;
                $order->pay_type = $pay_type;
                $order->ptime = strtotime($result_arr['time_end']);
                $order->save();
                $orderPay = OrderPay::getObjByOrderId($order->id);
                if(empty($orderPay)) {
                    $orderPay = new OrderPay();
                    $orderPay->order_id = $order->id;
                    $orderPay->enterprise_id = $order->enterprise_id;
                    $orderPay->shop_id = $order->shop_id;
                    $orderPay->device_no = $order->device_no;
                    $orderPay->cashier_id = $order->cashier_id;
                }
                $orderPay->pay_type = $pay_type;
                $orderPay->pay_fee = $result_arr['total_fee'];
                $orderPay->discount_fee = $order->discount_fee;
                $orderPay->payee = isset($result_arr['sub_mch_id']) ? $result_arr['sub_mch_id'] : $result_arr['mch_id'];
                $orderPay->payer = isset($result_arr['sub_openid']) ? $result_arr['sub_openid'] : $result_arr['openid'];
                $orderPay->trade_id = $result_arr['transaction_id'];
                $orderPay->utime = strtotime($result_arr['time_end']);
                $orderPay->save(0);
                break;
            }
            case "PAYERROR":
            {
                break;
            }
            case "USERPAYING":
            {
                break;
            }
            default:
                break;
        }
        return $order;
    }*/
}